14.3 Skalierung von Daten#

Lernziele

  • Sie wissen, dass Daten skaliert werden sollten. Ausnahme: Decision Tree oder Randon Forest.

  • Sie können Daten normieren.

  • Sie können Daten standardisieren.

Beispiel: Weinqualität#

Der folgende Datensatz stammt vom UCI Machine Learning Repository

https://archive.ics.uci.edu/dataset/186/wine+quality

und wurde ursprünglich in dieser Publikation betrachtet: http://www3.dsi.uminho.pt/pcortez/wine5.pdf

Input sind physikalische und chemische Messungen, Output ist die Qualität des Weines von 0 (sehr schlecht) bis 10 (exzellent). Wie üblich laden wir den Datensatz:

import pandas as pd 

data = pd.read_csv('data/winequality_red_DE.csv', skiprows=2)
data.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 1599 entries, 0 to 1598
Data columns (total 12 columns):
 #   Column                  Non-Null Count  Dtype  
---  ------                  --------------  -----  
 0   fester Säuregehalt      1599 non-null   float64
 1   flüchtiger Säuregehalt  1599 non-null   float64
 2   Zitronensäure           1599 non-null   float64
 3   Restzucker              1599 non-null   float64
 4   Chloride                1599 non-null   float64
 5   freies Schwefeldioxid   1599 non-null   float64
 6   Gesamtschwefeldioxid    1599 non-null   float64
 7   Dichte                  1599 non-null   float64
 8   pH-Wert                 1599 non-null   float64
 9   Sulfate                 1599 non-null   float64
 10  Alkohol                 1599 non-null   float64
 11  Qualität                1599 non-null   int64  
dtypes: float64(11), int64(1)
memory usage: 150.0 KB

Der Datensatz zur Weinqualität enthält 1599 Einträge mit 12 Eigenschaften. Die ersten 11 Eigenschaften werden durch Floats repräsentiert, nur die letzte Eigenschaft ‘Qualität’ wird durch Integers repräsentiert. Alle Einträge sind gültig.

data.head()
fester Säuregehalt flüchtiger Säuregehalt Zitronensäure Restzucker Chloride freies Schwefeldioxid Gesamtschwefeldioxid Dichte pH-Wert Sulfate Alkohol Qualität
0 7.4 0.70 0.00 1.9 0.076 11.0 34.0 0.9978 3.51 0.56 9.4 5
1 7.8 0.88 0.00 2.6 0.098 25.0 67.0 0.9968 3.20 0.68 9.8 5
2 7.8 0.76 0.04 2.3 0.092 15.0 54.0 0.9970 3.26 0.65 9.8 5
3 11.2 0.28 0.56 1.9 0.075 17.0 60.0 0.9980 3.16 0.58 9.8 6
4 7.4 0.70 0.00 1.9 0.076 11.0 34.0 0.9978 3.51 0.56 9.4 5

Ein erster Blick auf die Daten zeigt bereits, dass die Eigenschaftswerte in unterschiedlichen Bereichen liegen. Der feste Säuregehalt beispielsweise scheint zwischen 7 und 11 zu liegen, wohingegen Cloride scheinbar eher im Bereich 0.076 bis 0.098 liegen. Das zeigt auch die Übersicht der statistischen Kennzahlen:

data.describe()
fester Säuregehalt flüchtiger Säuregehalt Zitronensäure Restzucker Chloride freies Schwefeldioxid Gesamtschwefeldioxid Dichte pH-Wert Sulfate Alkohol Qualität
count 1599.000000 1599.000000 1599.000000 1599.000000 1599.000000 1599.000000 1599.000000 1599.000000 1599.000000 1599.000000 1599.000000 1599.000000
mean 8.319637 0.527821 0.270976 2.538806 0.087467 15.874922 46.467792 0.996747 3.311113 0.658149 10.422983 5.636023
std 1.741096 0.179060 0.194801 1.409928 0.047065 10.460157 32.895324 0.001887 0.154386 0.169507 1.065668 0.807569
min 4.600000 0.120000 0.000000 0.900000 0.012000 1.000000 6.000000 0.990070 2.740000 0.330000 8.400000 3.000000
25% 7.100000 0.390000 0.090000 1.900000 0.070000 7.000000 22.000000 0.995600 3.210000 0.550000 9.500000 5.000000
50% 7.900000 0.520000 0.260000 2.200000 0.079000 14.000000 38.000000 0.996750 3.310000 0.620000 10.200000 6.000000
75% 9.200000 0.640000 0.420000 2.600000 0.090000 21.000000 62.000000 0.997835 3.400000 0.730000 11.100000 6.000000
max 15.900000 1.580000 1.000000 15.500000 0.611000 72.000000 289.000000 1.003690 4.010000 2.000000 14.900000 8.000000

Schwankt beispielsweise die Dichte zwischen 0.990070 und 1.003690, so liegen die Gesamtschwefeldioxid-Werte zwischen 6 und 289 in einer völlig anderen Größenordnung.

Damit ist auch der Boxplot nicht mehr lesbar:

import plotly.express as px 

fig = px.box(data,
             title='Eigenschaften Rotwein',
             labels={'variable': 'Eigenschaft', 'value': 'Werte'})
fig.show()

Das hat auch Auswirkungen auf das Training der ML-Modelle.

Zunächst interpretieren wir die Prognose der Weinqualität als Klassifikationsproblem und setzen eine 1 für guten Wein (Qualität 6 und mehr) und eine 0 für schlechten Wein (Qualität bis einschließlich 5).

data_classification = data.copy()

data_classification.replace(3, 0, inplace=True) # schlechter Wein
data_classification.replace(4, 0, inplace=True) # schlechter Wein
data_classification.replace(5, 0, inplace=True) # schlechter Wein
data_classification.replace(6, 1, inplace=True) # guter Wein
data_classification.replace(7, 1, inplace=True) # guter Wein
data_classification.replace(8, 1, inplace=True) # guter Wein

Als nächstes trainieren wir ein neuronales Netz.

# Adaption der Daten
from sklearn.model_selection import train_test_split

X = data_classification.loc[:, 'fester Säuregehalt' : 'Alkohol']
y = data_classification['Qualität']

X_train, X_test, y_train, y_test = train_test_split(X,y, random_state=42)

# Training neuronales Netz
from sklearn.neural_network import MLPClassifier
from sklearn.model_selection import train_test_split

# Auswahl des Models
# solver = 'lbfgs' für kleine Datenmengen, solver = 'adam' für große Datenmengen, eher ab 10000
# hidden_layer: Anzahl der Neuronen pro verdeckte Schicht und Anzahl der verdeckten Schichten
model = MLPClassifier(solver='lbfgs', hidden_layer_sizes=[5, 5], random_state=42)

# Training
model.fit(X_train, y_train)

# Validierung 
score_train = model.score(X_train, y_train)
score_test = model.score(X_test, y_test)
print(f'Score für Trainingsdaten: {score_train:.2f}')
print(f'Score für Testdaten: {score_test:.2f}')
Score für Trainingsdaten: 0.75
Score für Testdaten: 0.73
/opt/homebrew/Caskroom/miniconda/base/envs/python312/lib/python3.12/site-packages/sklearn/neural_network/_multilayer_perceptron.py:545: ConvergenceWarning:

lbfgs failed to converge (status=1):
STOP: TOTAL NO. of ITERATIONS REACHED LIMIT.

Increase the number of iterations (max_iter) or scale the data as shown in:
    https://scikit-learn.org/stable/modules/preprocessing.html

Auf der einen Seite erhalten wir ein Resultat, aber auf der anderen Seite gibt es auch eine Fehlmeldung.

lbfgs failed to converge (status=1): STOP: TOTAL NO. of ITERATIONS REACHED LIMIT.

Tatsächlich ist der Algorithmus nicht zu einem “richtigen” Ergebnis gekommen, er ist nicht konvergiert. Scikit-Learn schlägt auch vor, wie wir den Algorithmus unterstützen können. Wir könnten die Anzahl der Iterationen erhöhen in der Hoffnung, dass dann der Algorithmus ein konvergentes Ergebnis erreicht, oder die Daten skalieren.

Wir betrachten daher beide Möglichkeiten. Zuerst setzen wir die Anzahl der Iterationen hoch. Die Dokumentation von Scikit-Learn gibt an, dass der Parameter ‘max_iter’ heißt und normalerweise auf 200 gesetzt ist. Wir setzen ihn auf 10000:

model = MLPClassifier(solver='lbfgs', hidden_layer_sizes=[5, 5], max_iter=10000, random_state=42)

# Training
model.fit(X_train, y_train)

# Validierung 
score_train = model.score(X_train, y_train)
score_test = model.score(X_test, y_test)
print(f'Score für Trainingsdaten: {score_train:.2f}')
print(f'Score für Testdaten: {score_test:.2f}')
Score für Trainingsdaten: 0.76
Score für Testdaten: 0.73

Hat funktioniert :-) Als nächstes betrachten wir noch das Skalieren der Daten. Außer bei Decision Trees / Random Forests sollten Daten immer skaliert werden.

Skalieren von Daten#

Sind die Bereich der Daten von ihren Zahlenwerten sehr verschieden, sollten alle numerischen Werte in dieselbe Größenordnung gebracht werden. Dieser Vorgang heißt Skalieren der Daten. Gebräulich sind dabei zwei verschiedene Methoden:

  • Normierung und

  • Standardisierung.

Normierung#

Bei der Normierung wird festgelegt, dass alle Zahlenwerte in einem festen Intervall liegen. Besonders häufig wrid das Intervall \([0,1]\) genommen. Die Dichte, die zwischen 0.990070 und 1.003690 liegt, würde so transformiert werden, dass das Minimum 0.990070 der 0 entspricht und das Maximum 1.003690 der 1. Genauso würde mit den anderen Eigenschaften verfahren werden. Wir nutzen zur praktischen Umsetzung Scikit-Learn.

from sklearn.preprocessing import MinMaxScaler

# Auswahl Normierung 
normierung = MinMaxScaler()

# Analyse: jede Spalte wird auf ihr Minimum und ihre Maximum hin untersucht
# es werden immer die Trainingsdaten verwendet
normierung.fit(X_train)

# Transformation der Trainungs- und Testdaten
X_train_normiert = normierung.transform(X_train)
X_test_normiert = normierung.transform(X_test)

Wir schauen in ‘X_train_normiert’ hinein:

print(X_train_normiert)
[[0.73584906 0.25342466 0.49       ... 0.79551122 0.03680982 0.12307692]
 [0.55345912 0.32876712 0.29       ... 0.83790524 0.07361963 0.10769231]
 [0.44654088 0.32191781 0.         ... 0.85286783 0.11042945 0.47692308]
 ...
 [0.45283019 0.34246575 0.06       ... 0.87531172 0.10429448 0.16923077]
 [0.49685535 0.05479452 0.35       ... 0.82793017 0.26380368 0.53846154]
 [0.36477987 0.11643836 0.26       ... 0.84538653 0.10429448 0.78461538]]

Es werden zwar nicht alle Werte angezeigt, aber die Normierung der Daten scheint funktioniert zu haben.

Jetzt trainieren wir das neuronale Netz erneut.

# Auswahl des Models
# solver = 'lbfgs' für kleine Datenmengen, solver = 'adam' für große Datenmengen, eher ab 10000
# hidden_layer: Anzahl der Neuronen pro verdeckte Schicht und Anzahl der verdeckten Schichten
model = MLPClassifier(solver='lbfgs', hidden_layer_sizes=[5, 5], max_iter=10000, random_state=42)

# Training
model.fit(X_train_normiert, y_train)

# Validierung 
score_train = model.score(X_train_normiert, y_train)
score_test = model.score(X_test_normiert, y_test)
print(f'Score für Trainingsdaten: {score_train:.2f}')
print(f'Score für Testdaten: {score_test:.2f}')
Score für Trainingsdaten: 0.79
Score für Testdaten: 0.73

Der Score der Traingsdaten ist leicht besser geworden.

Standardisierung#

Oft sind Daten normalverteilt. Die Standardisierung berücksichtigt das und transformiert nicht auf ein festes Intervall, sondern verschiebt den Mittelwert auf 0 und die Varianz auf 1. Die normalverteilten Daten werden also standardnormalverteilt. Auch das lassen wir Scikit-Learn erledigen:

from sklearn.preprocessing import StandardScaler

# Auswahl Normierung 
skalierung = StandardScaler()

# Analyse: jede Spalte wird auf ihr Minimum und ihre Maximum hin untersucht
# es werden immer die Trainingsdaten verwendet
skalierung.fit(X_train)

# Transformation der Trainungs- und Testdaten
X_train_skaliert = skalierung.transform(X_train)
X_test_skaliert = skalierung.transform(X_test)

print(X_train_skaliert)
[[ 1.5132922  -0.23260309  1.11458849 ... -0.45444422 -1.3131938
  -1.15257747]
 [ 0.36071333  0.37802632  0.09088663 ...  0.23998089 -0.97064635
  -1.24703683]
 [-0.31493635  0.32251456 -1.39348108 ...  0.4850721  -0.62809889
   1.01998773]
 ...
 [-0.27519225  0.48904985 -1.08637052 ...  0.85270892 -0.68519014
  -0.8691994 ]
 [ 0.00301644 -1.84244427  0.39799719 ...  0.07658675  0.79918216
   1.39782516]
 [-0.83160964 -1.34283839 -0.06266865 ...  0.36252649 -0.68519014
   2.90917487]]
# Auswahl des Models
# solver = 'lbfgs' für kleine Datenmengen, solver = 'adam' für große Datenmengen, eher ab 10000
# hidden_layer: Anzahl der Neuronen pro verdeckte Schicht und Anzahl der verdeckten Schichten
model = MLPClassifier(solver='lbfgs', hidden_layer_sizes=[5, 5], max_iter=10000, random_state=42)

# Training
model.fit(X_train_skaliert, y_train)

# Validierung 
score_train = model.score(X_train_skaliert, y_train)
score_test = model.score(X_test_skaliert, y_test)
print(f'Score für Trainingsdaten: {score_train:.2f}')
print(f'Score für Testdaten: {score_test:.2f}')
Score für Trainingsdaten: 0.81
Score für Testdaten: 0.70

Der Score der Trainingsdaten hat sich leicht verbessert, der Score für die Testdaten ist dafür leicht gesunken. Die Skalierung der Daten hat also einen Einfluss auf die Performance der ML-Modelle.

Zusammenfassung und Ausblick#

Daten sollten immer skaliert werden, sofern nicht Decision Trees oder Random Forests betrachtet werden. Es gibt zwei Möglichkeiten, Daten zu skalieren: Normierung oder Standardisierung. Letzteres wird häufiger verwendet, hängt aber natürlich von der Art der Daten ab.